package map;

import java.awt.image.Raster;
import java.util.ArrayList;
import java.util.Random;
import java.util.Scanner;

import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiEvent;
import javax.sound.midi.Sequence;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.Track;

import midi.Escalas;
import midi.MidiManager;
import midi.Nota;
import midi.Player;
import util.Base;
import util.Fachada;

/*
 *
 * 03/01/2011 - ? - Ainda tem que resolver uma serie de problemas. Primeiro, a normalizacao nao pode
 * gerar valores como 0 para duracao da nota, por exemplo. Como lidar com pausas? Ou as notas serao
 * contiguas? 
 * 
 * 04/01/2011 - Se o arquivo for muito grande, talvez seja melhor permitir duas abordagens -
 * 1o: selecionar uma regiao da imagem que sera mapeada. 2o: minimizar a imagem.? 
 * 
 * 18/01/2011 - OK - Realizada troca de canais para tracks com sucesso. Melhor resultado. Implementado o algoritmo
 * secondo(?) com sucesso. 
 * 
 * 10/02/2011 - OK - Prestar atencao quando o volume da faixa estiver baixo, pode aparentar erro, quando nao eh.
 * 
 * 10/02/2011 - OK - Corrigir a questao dos valores RGB alterados pelo metodo getRGBValue(); 
 * 
 * 14/02/2011 - BUG - No mapeamento Tetha todas as faixas estao assumindo o valor do ultimo instrumento.
 * 
 * 09/08/2011 - OK - Remoção dos offset, pois acredito que podiam estar resultando em valores maiores que 127 para as notas, 
 * o que podia acarretar em estouros.
 * 
 * 10/08/2011 - BUG - Conferir o motivo das durações em ticks estar diferindo tanto da duração estabelecida nas constantes
 * à partir da duração inicial da semibreve!
 * 
 * 10/08/2011 - OK - Retornar a implementação de canais por faixa, ao invés de editar uma track por faixa. 
 * Assim a seleção de instrumentos e a concorrência funcionam corretamente, utilizando statusBytes;
 * 
 * TODO list:
 * 
 * Sugestao: Implementar os metodos de girar imagem para gerar mais resultados a partir de uma imagem.
 * 
 * Revisar os mapeamentos implementados e padronizar coisas comuns a todos, como a varredura de pixels.
 * 
 * Mapeamento theta funciona, mas muito sensivel a mudança de fator, BPM, e da resolução da imagem.
 * 
 * 23/09/2011 - OK - Corrigido um problema na seleção dos instrumentos, agora faixas executam corretamente. 
 * Algoritmo zero() executa apenas imagens pequenas, reduzidas, entre 50 e 100 px de largura e altura.
 * 
 * @author Pedro Guimaraes
 */

public class Mapeamento extends Base {

	public static int offsetTeclas = 40;
	public static int offsetVelocity = 40;

	/**
	 * Neste mapeamento, o importante sera pegar os divisores comuns da largura
	 * e da altura. Esses valores serão os fatores que definirao a area de
	 * pixels a ser realizada a medida que definirah o valor de uma nota.
	 * 
	 * @param raster
	 * @param track
	 */
	public static void primo(Raster raster, Sequence sequence) {
		Escalas escalas = new Escalas();

		Random random = new Random();

		// Largura da imagem:
		int numeroDeColunas = raster.getWidth();

		// Altura da imagem:
		int numeroDeLinhas = raster.getHeight();

		out("Linhas: " + numeroDeLinhas + " | Colunas: " + numeroDeColunas);

		ArrayList<Integer> mdc = Fachada.divisoresComuns(numeroDeColunas,
				numeroDeLinhas);

		out("Fatores: ");
		for (Integer i : mdc)
			System.out.print(i + " | ");

		out("\nEscolha o numero de instrumentos que deseja: ");

		Scanner scanner = new Scanner(System.in);

		int fator = scanner.nextInt();

		while (!(mdc.contains(new Integer(fator)))) {
			out("Fator inválido. Favor inserir um dos valores válidos: ");
			fator = scanner.nextInt();
		}

		out("Fator selecionado: " + fator);

		// Valores temporarios para soma:
		int tecla = 0, duracao = 0, intensidade = 0;

		/**
		 * Algoritmo "primo" [Vide arquivo de referencias]. Aqui, a varredura se
		 * dar� em �reas de um ret�ngulo proporcional ao ret�ngulo da imagem,
		 * com a sua �rea igual � �rea total da imagem dividida pelo fator
		 * selecionado. As vari�veis altura e base referem-se ao comprimento, em
		 * pixels, da �rea de imagem cuja m�dia dos pixels ser� realizada para
		 * extrair os componentes da nota. Neste algoritmo, o n�mero de canais e
		 * de notas por canais � o mesmo, igual ao fator selecionado.
		 */
		int altura = numeroDeLinhas / fator;

		int base = numeroDeColunas / fator;

		int nCanais = fator;

		int nNotasPorCanal = fator;

		int rgb[] = new int[3];

		// Laco que varre cada canal:
		for (int canal = 0; canal < nCanais; canal++) {
			Track track = sequence.createTrack();
			out("Track: " + (canal + 1));
			int coluna = 0;

			// Altera o instrumento da faixa:
			MidiManager.changeProgram(track, canal, random.nextInt(100));

			// Configura o inicio, em tempo, do canal:
			int inicio = 0;

			// Laco que varre as notas por canal (relacao ao numero de linhas):
			for (int nota = 0; nota < nNotasPorCanal; nota++) {
				// Configura a posicao inicial do pixel para a dada nota:
				coluna = base * nota;

				// Laco que vai varrer a �rea de uma nota:
				for (int i = 0; i < base; i++) {
					// Incrementa a coluna:
					coluna++;

					int linha = altura * canal;

					for (int j = 0; j < altura; j++) {
						// Incrementa a linha:
						linha++;

						// Garante que os valores limites n�o sejam
						// ultrapassados:
						if (linha < numeroDeLinhas && coluna < numeroDeColunas) {
							try {
								// getPixel(x, y, rgb):
								raster.getPixel(coluna, linha, rgb);
								tecla += rgb[0];
								duracao += rgb[1];
								intensidade += rgb[2];
							} catch (Exception e) {
								out("Erro: " + e.getMessage());
								out("Linha: " + linha + " | Coluna: " + coluna);
							}
						}
					}
				}
				// Realiza as m�dias:
				tecla = Math.round(tecla / (base * altura));
				duracao = Math.round(duracao / (base * altura));
				intensidade = Math.round(intensidade / (base * altura));

				// Normaliza os valores para audio e busca nota em escala
				// espec�fica:
				tecla = normalizaNota(tecla);

				duracao = constantes.getDuracao(normalizaDuracao(duracao));
				intensidade = normalizaIntensidade(intensidade);
				int fim = duracao + inicio;
				out("Tecla: " + tecla + " Inicio: " + inicio + " Fim: " + fim
						+ " Intensidade: " + intensidade);
				MidiManager.addNote(track, new Nota(0, 50 + tecla, intensidade,
						inicio, fim));
				inicio = fim;
			}
		}
	}

	public static void secondo(Raster raster, Sequence sequence) {
		Escalas escalas = new Escalas();

		Random random = new Random();

		// Largura da imagem:
		int numeroDeColunas = raster.getWidth();

		// Altura da imagem:
		int numeroDeLinhas = raster.getHeight();

		out("Linhas: " + numeroDeLinhas + " | Colunas: " + numeroDeColunas);

		ArrayList<Integer> mdc = Fachada.divisoresComuns(numeroDeColunas,
				numeroDeLinhas);

		out("Fatores: ");
		for (Integer i : mdc)
			System.out.print(i + " | ");

		out("\nEscolha o fator que deseja: ");

		Scanner scanner = new Scanner(System.in);

		int fator = scanner.nextInt();

		while (!(mdc.contains(new Integer(fator)))) {
			out("Fator invalido. Favor inserir um dos valores validos: ");
			fator = scanner.nextInt();
		}

		out("Fator selecionado: " + fator);

		// Valores temporarios para soma:
		int tecla = 0, duracao = 0, intensidade = 0;

		/**
		 * Algoritmo "secondo" [Vide arquivo de referencias]. Aqui, a varredura
		 * se dar� em �reas de um quadrado. O lado do quadrado ser� igual ao
		 * fator selecionado em pixels.
		 */
		int altura = fator;

		int base = fator;

		int nCanais = numeroDeLinhas / fator;

		int nNotasPorCanal = numeroDeColunas / fator;

		int rgb[] = new int[3];

		// Laco que varre cada canal:
		for (int canal = 0; canal < nCanais; canal++) {
			Track track = sequence.createTrack();
			out("Track: " + (canal + 1));
			int coluna = 0;

			// Altera o instrumento da faixa:
			MidiManager.changeProgram(track, canal + 1, random.nextInt(100));

			// Configura o inicio, em tempo, do canal:
			int inicio = 0;

			// Laco que varre as notas por canal (relacao ao numero de linhas):
			for (int nota = 0; nota < nNotasPorCanal; nota++) {
				// Configura a posicao inicial do pixel para a dada nota:
				coluna = base * nota;

				// Laco que vai varrer a �rea de uma nota:
				for (int i = 0; i < base; i++) {
					// Incrementa a coluna:
					coluna++;

					int linha = altura * canal;

					for (int j = 0; j < altura; j++) {
						// Incrementa a linha:
						linha++;

						// Garante que os valores limites n�o sejam
						// ultrapassados:
						if (linha < numeroDeLinhas && coluna < numeroDeColunas) {
							try {
								// getPixel(x, y, rgb):
								raster.getPixel(coluna, linha, rgb);
								tecla += rgb[0];
								duracao += rgb[1];
								intensidade += rgb[2];
							} catch (Exception e) {
								out("Erro: " + e.getMessage());
								out("Linha: " + linha + " | Coluna: " + coluna);
							}
						}
					}
				}
				// Realiza as m�dias:
				tecla = Math.round(tecla / (base * altura));
				duracao = Math.round(duracao / (base * altura));
				intensidade = Math.round(intensidade / (base * altura));

				// Normaliza os valores para audio e busca nota em escala
				// espec�fica, em C:
				tecla = escalas.getNotaPentatonicaMaior(tecla, 0);

				duracao = constantes.getDuracao(normalizaDuracao(duracao));
				intensidade = normalizaIntensidade(intensidade);
				int fim = duracao + inicio;
				out("Tecla: " + tecla + " Inicio: " + inicio + " Fim: " + fim
						+ " Intensidade: " + intensidade);
				MidiManager.addNote(track, new Nota(canal + 1, 40 + tecla,
						intensidade, inicio, fim));
				inicio = fim;
			}
		}
	}

	public static void zero(Raster raster, Sequence sequence,
			ArrayList<Integer> instrumentos, int combina, int escala, int tom) {

		// Largura da imagem:
		int numeroDeColunas = raster.getWidth();

		// Altura da imagem: int
		int numeroDeLinhas = raster.getHeight();

		// Quantidade de faixas simultaneas desejadas pelo usuário:
		int numeroDeFaixas = instrumentos.size();

		// Variavel temporaria que recebe a informacao do pixel:
		int[] rgb = new int[3];

		ArrayList<Nota> notas = new ArrayList<Nota>();

		// Laco que varre cada pixel da imagem e garante que nao tente varrer
		// fora da area da imagem, mesmo que para isso seja necessario ignorar
		// alguns valores:
		for (int i = 0; i < numeroDeLinhas; i++) {
			for (int j = 0; j < numeroDeColunas; j++) {
				Nota nota = new Nota();
				raster.getPixel(j, i, rgb);
				getRGBValues(combina, nota, rgb);

				// Calcula a media da vizinhanca
				int tecla = nota.getTecla();
				long duracao = nota.getDuracao();
				int intensidade = nota.getIntensidade();

				// Normaliza os valores da tecla buscando a nota de uma escala
				// especifica:
				switch(escala) {
					case 0:
						nota.setTecla(escalas.getNotaAtonica(tecla, tom));
						break;
					case 1:
						nota.setTecla(escalas.getNotaNaturalMaior(tecla, tom));
						break;
					case 2:
						nota.setTecla(escalas.getNotaNaturalMenor(tecla, tom));
						break;
					case 3:
						nota.setTecla(escalas.getNotaMenorHarmonica(tecla, tom));
						break;
					case 4:
						nota.setTecla(escalas.getNotaPentatonicaMaior(tecla, tom));
						break;
				}		

				// Normaliza os valores da duracao buscando nas constantes:
				nota.setDuracao(constantes
						.getDuracao(normalizaDuracao(duracao)));

				// Normaliza a intensidade:
				nota.setIntensidade(offsetVelocity
						+ normalizaIntensidade(intensidade));

				// Adiciona nota a lista de notas mapeadas.
				notas.add(nota);
			}
		}

		int numeroDeNotas = notas.size();

		int notasPorFaixa = Math.round(numeroDeNotas / numeroDeFaixas);

		int contadorNotas = 0;

		Track track = sequence.createTrack();

		// Para cada faixa:
		for (int canal = 1; canal <= numeroDeFaixas; canal++) {

			long inicio = 0;

			// Configura o instrumento da faixa(default == 0/piano):
			int instrumento = instrumentos.get(canal - 1);

			/**
			 * TODO Não é um player que é responsável por isso, é um
			 * MidiManager. mandar a track para o MidiManager logo no início,
			 * para evitar ter que ficar passando como parâmetro.
			 */
			MidiManager.changeProgram(track, canal, instrumento);

			// Configura as notas desta faixa:
			for (int n = 0; n < notasPorFaixa; n++) {

				Nota nota = notas.get(contadorNotas++);

				nota.setCanal(canal);

				// Configura o início da nota para a duração da anterior.
				nota.setInicio(inicio);

				// NoteON:
				ShortMessage sm = new ShortMessage();
				try {
					sm.setMessage(constantes.mapeamentoCanalNoteOn.get(nota
							.getCanal()), nota.getTecla(), nota
							.getIntensidade());
				} catch (InvalidMidiDataException i) {
					i.printStackTrace();
				}
				track.add(new MidiEvent(sm, inicio));

				inicio += nota.getDuracao();

				// NoteOFF:
				sm = new ShortMessage();
				try {
					sm.setMessage(constantes.mapeamentoCanalNoteOff.get(nota
							.getCanal()), nota.getTecla(), nota
							.getIntensidade());
				} catch (InvalidMidiDataException e) {
					e.printStackTrace();
				}
				track.add(new MidiEvent(sm, inicio));
			}
		}
	}

	/*
	 * Metodos de normalizacao fazem a conversso proporcional dos valores RGB
	 * (0~255) para os valores de intensidade, duracao e nota, levando-se em
	 * consideração os offsets. Por exemplo, para as notas, a conversão deve
	 * considerar um offset de 40, evitando notas muito graves, portanto a
	 * normalização deve gerar valores entre 0 e 60(+40), totalizando uma gama
	 * de notas possíveis entre 40 e 100.
	 */
	private static int normalizaIntensidade(int intensidade) {
		return (int) Math.round(intensidade * 0.235);
	}

	private static int normalizaDuracao(long duracao) {
		return (int) Math.round(duracao * 0.0235);
	}

	private static int normalizaNota(int nota) {
		return (int) Math.round(nota * 0.196078431);
	}

	public static void mapeamentoTheta(Raster raster, Sequence sequence,
			int fator, ArrayList<Integer> instrumentos, int combina) {

		ArrayList<Nota> notas = new ArrayList<Nota>();

		// Largura da imagem:
		int numeroDeColunas = raster.getWidth();

		// Altura da imagem: int
		int numeroDeLinhas = raster.getHeight();

		// Variavel theta:
		int theta = (fator * 2) + 1;

		// O divisor é igual ao quadrado da variavel theta.
		int divisor = theta * theta;

		// Quantidade de faixas simultaneas desejadas pelo usuário:
		int numeroDeFaixas = instrumentos.size();

		// Variavel temporaria que recebe a informacao do pixel:
		int[] rgb = new int[3];

		int limiteLinhas = (int) Math.round(numeroDeLinhas / theta);

		int limiteColunas = (int) Math.round(numeroDeColunas / theta);

		int numeroDeNotas = 0;

		// Laco que varre cada pixel da imagem e garante que nao tente varrer
		// fora da area da imagem, mesmo que para isso seja necessario ignorar
		// alguns valores:
		for (int i = 0; i < limiteLinhas; i = i + theta) {
			for (int j = 0; j < limiteColunas; j = j + theta) {
				Nota nota = new Nota();

				// Laco que varre a vizinhanca do pixel:
				for (int k = 0; k < theta; k++) {
					for (int l = 0; l < theta; l++) {
						raster.getPixel(i + k, j + l, rgb);
						getRGBValues(combina, nota, rgb);
					}
				}

				// Calcula a media da vizinhanca
				int tecla = (Math.round(nota.getTecla() / divisor));
				int duracao = (Math.round(nota.getDuracao() / divisor));
				int intensidade = (Math.round(nota.getIntensidade() / divisor));

				/*
				 * out("A**: Nota: " + tecla + " Duracao: " + duracao +
				 * " Intensidade: " + intensidade);
				 */

				// Normaliza os valores da tecla buscando a nota de uma escala
				// especifica:
				nota.setTecla(offsetTeclas + normalizaNota(tecla));

				// Normaliza os valores da duracao buscando nas constantes:
				nota.setDuracao(constantes
						.getDuracao(normalizaDuracao(duracao)));

				// Normaliza a intensidade:
				nota.setIntensidade(offsetVelocity
						+ normalizaIntensidade(intensidade));

				out("* Nota: " + getNomeTecla(nota) + " Duração: "
						+ getNomeDuracao(nota) + " Intensidade: "
						+ nota.getIntensidade());

				// Adiciona nota a lista de notas mapeadas.
				notas.add(nota);
			}
		}

		for (Nota n : notas) {
			out("Tecla: " + n.getTecla() + " Duracao: " + n.getDuracao()
					+ " Vol.: " + n.getIntensidade());
		}

		numeroDeNotas = notas.size();

		int notasPorFaixa = Math.round(numeroDeNotas / numeroDeFaixas);

		out("Notas por faixa: " + notasPorFaixa + " Número de faixas: "
				+ numeroDeFaixas);

		int contadorNotas = 0;

		Track track = sequence.createTrack();

		// Para cada faixa:
		for (int m = 1; m <= numeroDeFaixas; m++) {

			long inicio = 0;

			// Configura o banco do instrumento da faixa(default == 0):
			// Player.changeBank(track, 1);

			// Configura o instrumento da faixa(default == 0/piano):
			int instrumento = instrumentos.get(m - 1);

			// TODO Não é um player que é responsável por isso, é um
			// MidiMaganer.
			// mandar a track para o MidiManager logo no início, para evitar ter
			// que ficar passando como parâmetro.
			MidiManager.changeProgram(track, m, instrumento);

			// Configura as notas desta faixa:
			for (int n = 0; n < notasPorFaixa; n++) {

				Nota nota = notas.get(contadorNotas++);

				nota.setCanal(m);

				// Configura o início da nota para a duração da anterior.
				nota.setInicio(inicio);

				MidiManager.addNote(track, nota);

				inicio += nota.getDuracao();
			}
		}
	}

	/**
	 * Metodo que define qual sera a ordem na qual serao pegos os valores de R G
	 * B por pixel e mapeados para intensidade, duracao e nota.
	 * 
	 * @param combina
	 * @param tecla
	 * @param duracao
	 * @param intensidade
	 * @param rgb
	 */
	public static void getRGBValues(int combina, Nota nota, int rgb[]) {
		switch (combina) {
		case 0:
			nota.setTecla(nota.getTecla() + rgb[0]);
			nota.setDuracao(nota.getDuracao() + rgb[1]);
			nota.setIntensidade(nota.getIntensidade() + rgb[2]);
			break;
		case 1:
			nota.setTecla(nota.getTecla() + rgb[0]);
			nota.setDuracao(nota.getDuracao() + rgb[2]);
			nota.setIntensidade(nota.getIntensidade() + rgb[1]);
			break;
		case 2:
			nota.setTecla(nota.getTecla() + rgb[1]);
			nota.setDuracao(nota.getDuracao() + rgb[0]);
			nota.setIntensidade(nota.getIntensidade() + rgb[2]);
			break;
		case 3:
			nota.setTecla(nota.getTecla() + rgb[1]);
			nota.setDuracao(nota.getDuracao() + rgb[2]);
			nota.setIntensidade(nota.getIntensidade() + rgb[0]);
			break;
		case 4:
			nota.setTecla(nota.getTecla() + rgb[2]);
			nota.setDuracao(nota.getDuracao() + rgb[1]);
			nota.setIntensidade(nota.getIntensidade() + rgb[0]);
			break;
		case 5:
			nota.setTecla(nota.getTecla() + rgb[2]);
			nota.setDuracao(nota.getDuracao() + rgb[0]);
			nota.setIntensidade(nota.getIntensidade() + rgb[1]);
			break;
		}
	}
}
